<?php

/**
 * 自动生成代码类：涵盖vue前端代码和后端代码
 * 1、路由文件 资源模式
 * 2、控制器文件
 * 3、逻辑文件
 * 4、模型文件、校验文件
 * 5、前端vue页面
 * 6、前端vue请求js文件
 * 7、routes文件生成
 */
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;


class AdminCode extends Command {
    /**
     * The name and signature of the console command.
     *
     * @var string
     * @dir 参数为目录，目录定位到当前模块下的目录，如admin模块，则Admin目录下，对应的代码位置，控制层、逻辑层、模型层，均相同的位置
     * vue目录也响应的在views目录下创建，dir为次级目录，同时控制器名为文件名
     * vue中js文件名为文件名，同时控制器名为文件名
     * 如不传dir目录，则默认在各自的模块下创建(vue在views下创建,控制器为目录，文件名为index)
     * table = 需要操作的表格
     * debug = 调试模式, 每次文件删除重建
     * modules = 指定模块 1controller,2model,3service,4validate,5vue,6vueRequest,7updateRoutes,8enum
     * echo = 不输出到file, 直接echo在终端
     * 
     */
    protected $signature = 'admin:code {table} {dir?} {debug=false} {modules=1,2,3,4,5,8} {echo=false}';

    protected $description = '自动生成代码类：涵盖vue前端代码和后端代码';

    protected $table = '';                      // 对应的表
    protected $true_table= '';                  // 真实表名处理，驼峰法转下划线
    protected $table_name = '';                 // 表名
    protected $table_comment = '';              // 表注释
    protected $dir = '';                        // 代码存放目录
    protected $true_dir='';                     // 代码存放目录，驼峰法转下划线
    protected $debug = false;                   // 调试模式, 每次文件删除重建
    protected $modules = [/* 1,2,3,4,5,6 */];   // 指定模块 1controller,2model,3service,4validate,5vue,6vueRequest
    protected $echo = false;                    // 不输出到file, 直接echo在终端

    protected $paths = [                     // 文件夹路径
        // route, controller, model, enum, service, validate, vue_view, vue_request
    ];
    protected $date = '';
    private $EOL = PHP_EOL;

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct() {
        parent::__construct();
        $this->paths = collect([
            'route'         => base_path('app/Admin/routes.php'),
            'controller'    => app_path('Http/Controllers/Admin/'),
            'model'         => app_path('Models/'),
            'enum'          => app_path('Enums/'),
            'service'       => app_path('Services/'),
            'validate'      => app_path('Http/Requests/'),
            'vue_view'      => base_path('admin/src/views/'),
            'vue_request'   => base_path('admin/src/views/api/admin/'),
        ]);
        $this->paths->filter(fn($path, $key)=>!in_array($key, ['route']))->each(function(&$path, $key) {
            if (!is_dir($path)) {
                mkdir($path, recursive:true);
            }
        });
        $this->date = date('Y-m-d H:i:s', time());
    }

    private function initDir($path) {
        if (!is_dir($path)) {
            mkdir($path, recursive:true);
        }
    }

    public function handle() {
        if (config('app.debug', false) != true){
            $this->error('非本地环境，禁止操作');
            die();
        }
        $this->debug = eval('return '. $this->argument('debug') .';');
        $this->echo = eval('return '. $this->argument('echo') .';');
        if ($this->debug) {
            if ($this->confirm('debug模式, 每次文件删除重建', true)) {
                $this->debug = true;
            } else {
                $this->debug = false;
            }
        }
        echo 'debug is: '. $this->debug .PHP_EOL;

        $this->modules = explode(',', $this->argument('modules'));

        $table = ucfirst(Str::studly($this->argument('table')));
        $this->table = $table;
        $this->true_table = Str::snake($this->table);
//        $this->info($table);

        $dir = ucfirst($this->argument('dir'));
        $this->dir = $dir;
        $this->true_dir = Str::snake($this->dir);
        
        if (!empty($dir)){
            $this->paths->filter(fn($path, $key)=>!in_array($key, ['route', 'vue_request']))->each(function(&$path, $key) {
                $this->paths[$key] .= $key == 'vue_view' ? "{strtolower($this->dir)}/" : "{$this->dir}/";
            });
        }

        // 查看表信息
        $table_sql = "select table_name,table_comment from information_schema.TABLES where table_schema='". DB::getConfig('database') ."' and table_name='{$this->true_table}'";
        $table_res = DB::select($table_sql);

        if (empty($table_res)){
            $this->error('表不存在,无法生成代码');
            die();
        } else {
            $table_res = (array)$table_res[0];
            $this->table_comment = $table_res['TABLE_COMMENT'] ?? $table_res['table_comment'];
            $this->table_name = $table_res['TABLE_NAME'] ?? $table_res['table_name'];
        }

        $showDdl = ((array)DB::select("show create table `{$this->true_table}`")[0])['Create Table'];
        preg_match("/COMMENT='(.+)'/", $showDdl, $comment);
        preg_match_all("/KEY.*\((.+)\)/", $showDdl, $keys);
        // 索引 兼容联合索引
        $keys = collect($keys[1])->map(fn($it)=>collect(explode(',', $it))->map(fn($it)=>str_replace('`','',$it)))->sortBy(fn($it)=>count($it))->toArray();
        foreach($keys as $k=>$v){ $keys[$v[0]] = $v; unset($keys[$k]); }
        // var_dump($keys); die();
        // 表注释
        $comment = $comment[1] ?? $this->table;
        // var_dump($keys);
        // var_dump($comment); 
        // die();

        // COLUMN_NAME              列的名称
        // DATA_TYPE                列数据类型
        // COLUMN_TYPE              列数据类型 及精度或长度
        // CHARACTER_MAXIMUM_LENGTH 对于字符串列，以字符为单位的最大长度
        // COLUMN_COMMENT           注释
        // COLUMN_DEFAULT           列的默认值
        // IS_NULLABLE              列的可空性 YES NO
        // COLUMN_KEY               该列是否被索引 PRI主键 UNI唯一索引 MUL索引
        $sql = "SELECT `COLUMN_NAME`,`DATA_TYPE`,`COLUMN_TYPE`,`COLUMN_COMMENT`,`CHARACTER_MAXIMUM_LENGTH`,`COLUMN_DEFAULT`,`IS_NULLABLE`,`COLUMN_KEY`,`EXTRA` FROM `information_schema`.`COLUMNS` WHERE `table_schema`='". DB::getConfig('database') ."' AND `table_name`='{$this->true_table}' order by `ORDINAL_POSITION` asc";
        $ddl = DB::select($sql);
        $ddl = collect($ddl)->map(function($it)use($keys){ 

            $it = (array)$it;
            // print_r($it['COLUMN_DEFAULT']);
            // print_r($it); die();
            $it['name'] = $it['COLUMN_NAME'];
            $it['ucname'] = ucfirst(Str::camel($it['name']));
            // 主键
            $it['ispri'] = $it['COLUMN_KEY'] == 'PRI';
            // 索引
            if (array_key_exists($it['name'], $keys)) {
                $it['iskey'] = true;
                $it['iskeymul'] = array_slice($keys[$it['name']], 1);   // 处理联合索引
            } else {
                $it['iskey'] = boolval($it['COLUMN_KEY']);
            }
            // 可为空
            $it['isnullable'] = $it['IS_NULLABLE'] == 'YES';
            $it['nullv'] = $it['isnullable'] ? " ?? ''" : '';
            $it['defaultv'] = ($it['isnullable'] || isset($it['COLUMN_DEFAULT'])) ? " ?? '{$it['COLUMN_DEFAULT']}'" : '';
            // 可忽略
            $it['isignore'] = $it['ispri'] || preg_match('/(table|primaryKey|create_time|update_time|delete_time|created_at|updated_at|deleted_at|ctime|utime|dtime)/i', $it['COLUMN_NAME']) != null;
            $it['ignorev'] = $it['isignore'] ? '// ' : '';
            // 是图片
            $it['isimage'] = preg_match('/(image|cover|img|thumb|banner)/i', $it['COLUMN_NAME']) != null;
            // 判断是否为下拉选择 \d^\d
            preg_match_all('/([\d+|Y|N])([^\d|^Y|^N]+)/', $it['COLUMN_COMMENT'], $enums);
            // 状态列表 [0=>'未知', 1=>'男', 2=>'女']
            $it['enums'] = collect($enums)->filter(fn($it,$k)=>$it&&$k!=0)->map(fn($it)=>array_map(fn($it)=>str_replace([',','，','、',' '], '', $it), $it))->toArray();
            if ($it['enums']) {
                $it['enumslabel'] = str_replace(['(',')','（', '）','：'], '', substr($it['COLUMN_COMMENT'], 0, strpos($it['COLUMN_COMMENT'], current(current($enums)))));
                $it['enums'] = collect($it['enums'][1])->zip(collect(str_replace(['(',')','（', '）','：'], '', $it['enums'][2])))->sortBy(fn($it)=>$it[0])->toArray(); 
                $it['enumsn'] = 'Enum'. $it['ucname'];
            }
            // $it['isenum'] = preg_match('/(state|status)/i', $it['COLUMN_NAME']) != null || $it['COLUMN_TYPE'] == 'tinyint(1)';
            $it['eq'] = !$it['enums'] && preg_match('/(char|varchar|tinytext)/i', $it['DATA_TYPE']) != null ? 'like' : '=';
            $it['eq%'] = !$it['enums'] && preg_match('/(char|varchar|tinytext)/i', $it['DATA_TYPE']) != null ? "%" : "";
            // 字符串
            $it['isstr']  = preg_match('/(char|varchar|tinytext|text|mediumtext|longtext)/i', $it['DATA_TYPE']) != null;
            // 长字符串
            $it['islongstr'] = preg_match('/(text|mediumtext|longtext)/i', $it['DATA_TYPE']) != null || $it['CHARACTER_MAXIMUM_LENGTH'] > 200;
            // 数字
            $it['isdigit']  = preg_match('/(tinyint|smallint|mediumint|int|integer|bigint)/i', $it['DATA_TYPE']) != null;
            // 金额
            $it['ismoney'] = preg_match('/(price|money|amonut)/i', $it['COLUMN_NAME']) != null || preg_match('/(float|double|decimal)/i', $it['DATA_TYPE']) != null;
            // 日期时间
            $it['isdatetime']  = preg_match('/(datetime|timestamp)/i', $it['DATA_TYPE']) != null;
            // 时间
            $it['istime']  = preg_match('/(time)/i', $it['DATA_TYPE']) != null;
            // 日期
            $it['isdate']  = preg_match('/(date)/i', $it['DATA_TYPE']) != null;
            // 年
            $it['isyear']  = preg_match('/(year)/i', $it['DATA_TYPE']) != null;
            return $it; 
        });
        $ddl = [
            'comment'   => $comment,
            '_dir'      => empty($this->dir) ? '' : '\\'.$this->dir,
            '_dir1'     => empty($this->dir) ? '' : $this->dir.'/',
            'pri'       => $ddl->filter(fn($it)=>$it['ispri'])[0]['COLUMN_NAME'],
            'ucpri'     => Str::studly($ddl->filter(fn($it)=>$it['ispri'])[0]['COLUMN_NAME']),
            'pri_comment'     => Str::studly($ddl->filter(fn($it)=>$it['ispri'])[0]['COLUMN_COMMENT']),
            'keys'      => $ddl->filter(fn($it)=>$it['iskey'])->map(fn($it)=>$it['COLUMN_NAME'])->toArray(),
            'ddl'       => $ddl,
        ];
        // dd($ddl);
//        $this->info(json_encode($res,JSON_UNESCAPED_UNICODE));
        $this->line('开始生成代码');
        if (!$this->echo && in_array(7, $this->modules)) {
            $this->updateRoute();
        }
        foreach($this->modules as $m) {
            switch($m) {
                case 1:
                    $this->createController($ddl);
                    break;
                case 2:
                    $this->createModel($ddl);
                    break;
                case 3:
                    $this->createService($ddl);
                    break;
                case 4:
                    $this->createValidate($ddl);
                    break;
                case 5:
                    $this->createVue($ddl);
                    break;
                case 6:
                    $this->createVueRequest($ddl);
                    break;
                case 8:
                    $this->createEnum($ddl);
                    break;
            }
        }
        $this->line(PHP_EOL.'生成代码完成');

    }

    /**更新admin路由文件
     * @return void
     * @date  2023-04-10 16:16
     * @author dapeng
     */
    public function updateRoute() {
        if (!file_exists($this->paths['route'])) {
            $this->error('路由文件不存在: '. $this->paths['route']); die();
        }
        $file_arr = collect(file($this->paths['route']))->map(function($it){ return rtrim($it); });
        $line = intval($file_arr->search('    // end-admin-route'));
        if (!$line) {
            $this->error('    // end-admin-route 结束标志不存在'); die();
        }
        // $dir = $this->dir ? strtolower($this->dir) : '';
        $_dir = $this->dir ? '/'.strtolower($this->dir) : '';
        $_dir2 = $this->dir ? "\\$this->dir" : '';
        $_router = "    /*{$this->table_comment} 后台界面*/Route::get('{$_dir}/{$this->true_table}', '\App\Http\Controllers\Admin$_dir2\\$this->table"."Controller@home');";
        $_router_api = "    Route::resource('/api{$_dir}/{$this->true_table}', '\App\Http\Controllers\Admin$_dir2\\$this->table"."Controller'); // {$this->table_comment} 相关的路由";
        $_router_export = "    Route::get('/api{$_dir}/{$this->true_table}/export', '\App\Http\Controllers\Admin$_dir2\\$this->table" . "Controller@export'); // {$this->table_comment} 相关的路由@导出数据";
        $_router_enum = "    Route::get('/api{$_dir}/{$this->true_table}/enum/{type}', '\App\Http\Controllers\Admin$_dir2\\$this->table" . "Controller@enum'); // 获取枚举数据";
        $_router_upload = "    Route::post('/api{$_dir}/{$this->true_table}/upload', '\App\Http\Controllers\Admin$_dir2\\$this->table" . "Controller@upload'); // 相关文件上传";

        $chunks = $file_arr->chunk($line);
        if ($file_arr->search($_router)) {
            $this->warn('home路由已存在，无需更新');
        } else {
            $chunks[0]->push($_router);
        }
        if ($file_arr->search($_router_export)) {
            $this->warn('export路由已存在，无需更新');
        } else {
            $chunks[0]->push($_router_export);
        }
        if ($file_arr->search($_router_enum)) {
            $this->warn('enum路由已存在，无需更新');
        } else {
            $chunks[0]->push($_router_enum);
        }
        if ($file_arr->search($_router_upload)) {
            $this->warn('upload路由已存在，无需更新');
        } else {
            $chunks[0]->push($_router_upload);
        }
        if ($file_arr->search($_router_api)) {
            $this->warn('resource路由已存在，无需更新');
        } else {
            $chunks[0]->push($_router_api);
        }

        $file_arr = $chunks->collapse();
        if ($line != intval($file_arr->search('    // end-admin-route'))) {
            file_put_contents($this->paths['route'], join(PHP_EOL, $file_arr->toArray()));
            $this->info('路由文件已更新');
        } else {
            $this->warn('路由文件不需要更新');
        }
    }

    /**控制器生成代码
     * @param $table
     * @return void
     * @date  2023-04-12 16:16
     * @author dapeng
     */
    public function createController($ddl) {
        $file = $this->paths['controller']. $this->table.'Controller.php';
        if (file_exists($file) && !$this->debug){
            $this->warn('控制器文件已存在');
            return false;
        }
        $exportTitle = $ddl['ddl']->map(fn($it)=>"'{$it['COLUMN_COMMENT']}'")->join(', ');
        $exportSelect = $ddl['ddl']->map(fn($it)=>"'{$it['name']}'")->join(', ');
        $exportObj = $ddl['ddl']->map(fn($it)=>"\$obj->{$it['name']}")->join(', ');
        
        $ddl['ddl']->filter(fn($it) => $it['enums'])->each(function($it)use(&$props){
            $props .= <<<EOF
            '{$it['name']}'        => {$this->table}Model::{$it['enumsn']},
EOF.PHP_EOL;
        });
        $props = rtrim($props);
        // dd($ddl);
        $content = <<<EOF
<?php

namespace App\Http\Controllers\Admin{$ddl['_dir']};

use App\Http\Controllers\Controller;
use App\Services{$ddl['_dir']}\\{$this->table}Service;
use App\Models{$ddl['_dir']}\\{$this->table}Model;
use App\Validate{$ddl['_dir']}\\{$this->table}Validate;
use Exception;

/**
* {$ddl['comment']} 控制器
* @create {$this->date}
*/
class {$this->table}Controller extends Controller {

        
    /** 分页列表 */
    public function index() {
        \$rows = {$this->table}Model::selectList(\$this->pag(), request()->all());
        return self::bean(\$rows);
    }
            
    /** 新增 */
    public function store() {
        {$this->table}Validate::scene('store');
        
        \$row = {$this->table}Model::insertData(request()->all());
        return self::bean(\$row);
    }

    /** 更新 */
    public function update() {
        {$this->table}Validate::scene('update');
        
        \$row = {$this->table}Model::updateData(request('id'), request()->all());
        return self::bean(\$row);
    }
            
    /** 详情 */
    public function show(\${$ddl['pri']}) {
        \$row = {$this->table}Model::selectBy{$ddl['ucpri']}(\${$ddl['pri']});
        return self::bean(\$row);
    }
            
    /** 删除 */
    public function destroy(\${$ddl['pri']}s) {
        \${$ddl['pri']}s = explode(',', \${$ddl['pri']}s);
        \$suc = []; \$err = [];
        foreach(\${$ddl['pri']}s as \$id) {
            try {
                \$suc[] = {$this->table}Model::deleteData(\$id);
            } catch(Exception \$e) {
                \$err[] = ['{$ddl['pri']}'=>\$id, 'msg'=>\$e->getMessage()];
            }
        }
        if (count(\$err) == count(\${$ddl['pri']}s)) {
            return self::beanErr(collect(\$err)->map(fn(\$it)=>"{\$it['id']} {\$it['msg']}")->first(), 500);
        }
        return self::bean(['success'=>\$suc, 'error'=>\$err]);
    }

    /** 文件上传 */
    public function upload() {
        \$id = request('id');                // 为空是上传
        \$type = request('type');            // 可为空
        \$file = request()->file('file');
        
        \$dir = '/upload/{$this->true_dir}/{$this->true_table}';
        \$ext = strtolower(\$file->getClientOriginalExtension()) ?: 'png';
        \$name = date('YmdHisu') .'_'. rand(1000, 9999) .'.'. \$ext;
        \$file = \$file->move(public_path(\$dir), \$name);
        return \$this->bean([
            'type'          => \$type,
            'url'           => \$dir .DIRECTORY_SEPARATOR. \$name,
            'name'          => \$name,
            'size'          => \$file->getSize(),
            'ext'           => \$file->guessExtension(),
            'mime'          => \$file->getMimeType(),
        ]);
    }
            
    /** excel导出 - 数据大易锁表, 主从? */
    public function export() {
        \$_type = request('_type');
        \$params = request()->except(['_type']);

        if (\$_type == 'sms') {
            // \$csvName = '{$this->true_table}' .'-'. \$_type .'-'. date('Y-m-d') .'-'. rand(100,999) .'.csv';
            // \$sheetTitle = [];
            // \$query = {$this->table}Model::query()
            //     ->select([])
            //     ->when(isset(\$params['id']), fn(\$q)=>\$q->where('id', '=', \$params['id']))
            //     ;
            // export_csv(\$csvName, \$sheetTitle, \$query, fn(\$obj) => [
            //     \$obj->id, 
            // ]);
        }

        \$csvName = '{$this->true_table}' .'-'. date('Y-m-d') .'-'. rand(100,999) .'.csv';
        \$sheetTitle = [{$exportTitle}];
        \$query = {$this->table}Model::query()
                ->select([{$exportSelect}])
                // ->when(isset(\$params['id']), fn(\$q)=>\$q->where('id', '=', \$params['id']))
                ;
        export_csv(\$csvName, \$sheetTitle, \$query, fn(\$obj) => [
            {$exportObj}
        ]);
    }
            
}
EOF;
        if ($this->echo) {
            echo $content; return;
        }
        file_put_contents($file, $content);
        $this->info('控制器生成成功');
    }

    /**生成模型文件
     * @param $res
     * @return false|void
     * @date  2023-04-13 14:14
     * @author dapeng
     */
    public function createModel($ddl) {
        $file = $this->paths['model'] . $this->table . 'Model.php';
        if (file_exists($file) && !$this->debug) {
            $this->warn('模型文件已存在');
            return false;
        }
        $timestamps = $ddl['ddl']->filter(fn($it)=>preg_match('/(table|primaryKey|create_time|update_time|delete_time|created_at|updated_at|deleted_at|ctime|utime|dtime)/i', $it['name']) == 1)->isNotEmpty();
        $timestamps = $timestamps ? 'true' : 'false';
        // enums
        $enums_use = '';
        if ($ddl['ddl']->filter(fn($it)=>$it['enums'])->isNotEmpty()) {
            // use App\Enums\System\SysConfig\{Status, DelFlag};
            $enums_use = "use App\Enums{$ddl['_dir']}\\{$this->table}\{". $ddl['ddl']->filter(fn($it)=>$it['enums'])->map(fn($it)=>$it['ucname'])->join(', ') ."};";
        }
        // selectBy
        $ddl['ddl']->filter(fn($it)=>$it['iskey'])->each(function($it)use(&$selectBy,$ddl){
            $mulk = '';
            $mul = '';
            foreach($it['iskeymul']??[] as $m) {
                $mulk .= ", \${$m}=null";
                $d = $ddl['ddl']->filter(fn($it)=>$it['name']==$m)->first();
                $mul .= PHP_EOL.<<<EOF
        if (\${$m}) \$where[] = ['{$d['name']}', '{$d['eq']}', "{$d['eq%']}{\${$d['name']}}{$d['eq%']}"];
EOF;
            }
            $selectBy .= <<<EOF
    /** 根据{$it['ucname']}获取 */
    public function selectBy{$it['ucname']}(\${$it['name']}{$mulk}): {$this->table}Model {
        \$where = [['{$it['name']}', '{$it['eq']}', "{$it['eq%']}{\${$it['name']}}{$it['eq%']}"]];{$mul}
        return throw_str(
            static::row(\$where) ?? "数据(\${$it['name']})为空~"
        );
    }
EOF.PHP_EOL;
        });
        // wheres whereseq
        $whereeq = $ddl['ddl']->filter(fn($it)=>($it['iskey']||$it['enums']) && $it['eq'] == '=')->map(fn($it)=>"'{$it['name']}'")->join(', ');
        $wheres = <<<EOF
                // static::\$Wheres['{$ddl['pri']}']\t= fn(\$q, \$v) => \$q->where(atn('{$ddl['pri']}'), '=', \$v);
        EOF.PHP_EOL;
        $ddl['ddl']->filter(fn($it)=>($it['iskey']||$it['enums']) && $it['eq'] != '=')->each(function($it)use(&$wheres){
            $wheres .= <<<EOF
                    static::\$Wheres['{$it['name']}']\t= fn(\$q, \$v) => \$q->where(atn('{$it['name']}'), '{$it['eq']}', "{$it['eq%']}{\$v}{$it['eq%']}");
            EOF.PHP_EOL;
        });
        $wheres = rtrim($wheres);
//         $ddl['ddl']->filter(fn($it)=>$it['iskey']||$it['enums'])->each(function($it)use(&$selectList){
//             $selectList .= <<<EOF
//                   ->when(isset(\$param['{}']), fn(\$q)=>\$q->where('{$it['name']}', '{$it['eq']}', "{$it['eq%']}{\$param['{$it['name']}']}{$it['eq%']}"))
// EOF.PHP_EOL;
//                 });
        // selectList
        $selectList = <<<EOF
    /** 获取列表 @return {$this->table}[]+paginator() */
    public function selectList(int|array \$pag=1, array \$param=[]): LengthAwarePaginatorProvider {
        return static::rows_pag(\$pag, [], fn(\$q)=>\$q
                    // ->when(isset(\$param['{$ddl['pri']}']), fn(\$q)=>\$q->where('{$ddl['pri']}', '=', \$param['{$ddl['pri']}']))
EOF.PHP_EOL;
        $selectList .= <<<EOF
                    ->wheres(\$param, {$ddl['ddl']->filter(fn($it)=>$it['iskey']||$it['enums'])->map(fn($it)=>"'{$it['name']}'")->join(',')})
EOF.PHP_EOL;
        $selectList .= <<<EOF
                    ->orderByDesc("{$ddl['pri']}")
        );
    }
EOF.PHP_EOL;
        // insertData
        $insertData = <<<EOF
    /** 插入数据 */
    public function insertData(array \$data): {$this->table}Model {
        return throw_str(
            static::row_add([
EOF.PHP_EOL;
        $ddl['ddl']->each(function($it)use(&$insertData){
            $insertData .= <<<EOF
                {$it['ignorev']}'{$it['name']}'\t\t=> \$data['{$it['name']}']{$it['defaultv']},
EOF.PHP_EOL;
        });
        $insertData .= <<<EOF
            ])
        );
    }
EOF.PHP_EOL;
        // updateData
        $updateData = <<<EOF
    /** 更新数据 */
    public function updateData(int|array \$idOrWhere, array \$data): {$this->table}Model {
        \$values = static::when(
                    \$data, 
EOF.PHP_EOL;
        $ddl['ddl']->each(function($it)use(&$updateData){
            $exp = $it['isignore'] ? '// ' : '';
            $updateData .= <<<EOF
                    {$exp}'{$it['name']}',
EOF.PHP_EOL;
        });
        $updateData .= <<<EOF
                );
        return throw_str(
            static::row_update(\$idOrWhere, \$values)
        );
    }
EOF.PHP_EOL;
        // deleteData
        $deleteData = <<<EOF
    /** 删除数据 */
    public function deleteData(int|array \$idOrWhere): {$this->table}Model {
        return throw_str(
            static::row_delete(\$idOrWhere)
        );
    }
EOF.PHP_EOL;

        // var_dump($ddl); die();
        $content = 
<<<EOF___
<?php
namespace App\Models{$ddl['_dir']};

use App\Exceptions\ApiException;
use App\Providers\LengthAwarePaginatorProvider;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
{$enums_use}

/**
* {$ddl['comment']} 模型
* @create {$this->date}
*/
class {$this->table}Model extends \App\Models\BaseModel {
    use HasFactory;

    const Table\t\t\t= "{$this->true_table}";
    const PrimaryKey\t= "{$ddl['pri']}";
    public \$timestamps\t= {$timestamps};

    static \$Wheres = [{$whereeq}];     // 直接定义column, 默认where(column, '=', value)
    public function __construct(\$attributes = []) {
        parent::__construct(\$attributes);

{$wheres}
    }

    /** xxxx */
    // public function user() {
        // return \$this->hasOne(User::class, 'id', 'uid');
    // }

    /** ========== 常用查询 ========== */
    static \$Vo\t= ['*', ];
    static \$Voi\t= ['*', ];

{$selectBy}

{$selectList}

{$insertData}

{$updateData}

{$deleteData}


}
EOF___;
        if ($this->echo) {
            echo $content; return;
        }
        file_put_contents($file, $content);
        $this->info('模型文件生成完成 ' . $file);
    }

    public function createEnum($ddl) {
        $ddl['ddl']->filter(fn($it)=>$it['enums'])->each(function($it) use($ddl) {
            $this->initDir($this->paths['enum'] . $this->table .'/');
            $file = $this->paths['enum'] . $this->table .'/'. $it['ucname'].'.php';
            if (file_exists($file) && !$this->debug) { 
                $this->warn('enum exists: '. $file); return true; 
            }
            $type = $it['isdigit'] ? 'int' : 'string';
            $mark = $it['isdigit'] ? '' : "'";
            $case = '';
            foreach($it['enums'] as $enum) {
                $deafult = $enum[0]==$it['COLUMN_DEFAULT'] ? ', DefaultCase' : '';
                $case .= 
<<<EOF___
    #[Localization(label:'{$enum[1]}'){$deafult}]
    case {$enum[1]}\t\t= {$mark}{$enum[0]}{$mark};{$this->EOL}

EOF___;
            }
            $content = 
<<<EOF___
<?php
namespace App\Enums{$ddl['_dir']}\\{$this->table};

use App\Enums\{EnumTrait, Localization, DefaultCase};

/** {$it['COLUMN_COMMENT']} from `{$this->true_table}` table */
enum {$it['ucname']} : {$type} {
    use EnumTrait;

{$case}
}
EOF___;
            if ($this->echo) {
                echo $content.PHP_EOL; return true;
            }
            file_put_contents($file, $content.PHP_EOL);
            $this->info('enum created: '. $file);
        });
        $this->info('enum文件生成完成');
    }

    /**生成service层文件
     * @param $res
     * @return void
     * @date  2023-04-13 11:11
     * @author dapeng
     */
    public function createService($ddl) {
        $file = $this->paths['service'] . $this->table.'Service.php';
        if (file_exists($file) && !$this->debug) {
            $this->warn('逻辑services文件已存在');
            return ;
        }

        $_table = lcfirst($this->table);

        $content = <<<EOF
<?php
namespace App\Services{$ddl['_dir']};

use App\Common\Autoinject;
use App\Models{$ddl['_dir']}\\{$this->table}Model;
use App\Validate{$ddl['_dir']}\\{$this->table}Validate;
use Illuminate\Database\Eloquent\Collection;
        
/**
* {$ddl['comment']} 逻辑层 前端API业务分装
* @create {$this->date}
*/
class {$this->table}Service extends \App\Services\BaseService {

    #[Autoinject]
    private {$this->table}Model \${$_table}Model;

}

EOF;
        if ($this->echo) {
            echo $content; return;
        }
        file_put_contents($file, $content);
        $this->info('逻辑services文件生成完成');
    }

    /**创建验证器
     * @param $res
     * @return void
     * @date  2023-04-13 15:15
     * @author dapeng
     */
    public function createValidate($ddl) {
        $this->initDir($this->paths['validate'] . $this->table);
        $file = $this->paths['validate'] . $this->table .'/'. $this->table.'Request.php';
        if (file_exists($file) && !$this->debug) {
            $this->warn('验证器文件已存在');
            return ;
        }
        // enums
        $enums_use = '';
        if ($ddl['ddl']->filter(fn($it)=>$it['enums'])->isNotEmpty()) {
            // use App\Enums\System\SysConfig\{Status, DelFlag};
            $enums_use = "use App\Enums{$ddl['_dir']}\\{$this->table}\{". $ddl['ddl']->filter(fn($it)=>$it['enums'])->map(fn($it)=>$it['ucname'])->join(', ') ."};";
        }

        $ddl['ddl']->each(function($it) use(&$rules, &$rulestr, &$store, &$update, &$property, &$messages) {
            $_rules_value = ["'required'"];
            if ($it['enums']) $_rules_value[] = "new Enum({$it['ucname']}::class)";
            if ($it['ismoney']) $_rules_value[] = "'min:0'";
            $_rules_value = join(', ', $_rules_value);
            $prefix = $it['isnullable'] || $it['isignore'] ? '// ' : '';
            if ($it['COLUMN_KEY'] == 'PRI') $prefix = '';
            $property .= 
<<<EOF___
    $prefix#[OA\Property('{$it['name']}', title: '{$it['COLUMN_COMMENT']}', type: 'string', example: '', description: '')]{$this->EOL}
EOF___;
            if ($it['COLUMN_KEY'] == 'PRI') {
                $rules .= 
<<<EOF___
            ...(request()->isMethod('put') ? ['{$it['name']}' => [{$_rules_value}]] : []),{$this->EOL}
EOF___;
            } else {
                $rules .= 
<<<EOF___
            $prefix'{$it['name']}' => [{$_rules_value}],{$this->EOL}
EOF___;
            }
            $rulestr .= 
<<<EOF___
            '{$it['name']}' => '{$it['COLUMN_COMMENT']}',{$this->EOL}
EOF___;
            if ($it['enums']) {
                $messages .=
<<<EOF___
            '{$it['name']}.Illuminate\Validation\Rules\Enum' => \$this->attributes()['{$it['name']}'].' 值必须为: '. join(',', {$it['ucname']}::values()),{$this->EOL}
EOF___;
            }
            $k1 = 'key';
            $k2 = 'key';
            if ($it['isnullable']) {
                $k1 = 'isnullable';
                $k2 = 'isnullable';
            } else if ($it['isignore']) {
                $k1 = 'isignore';
                if (!$it['ispri']) $k2 = 'isignore';
            }
            $store[$k1] = ($store[$k1] ?? '') . "'{$it['name']}',";
            $update[$k2] = ($update[$k2] ?? '') . "'{$it['name']}',";
        });
        $property = rtrim($property);
        $rules = rtrim($rules);
        $rulestr = rtrim($rulestr);
        $messages = rtrim($messages);
        $store['isnullable'] = $store['isnullable'] ?? '';
        $store['isignore'] = $store['isignore'] ?? '';
        $store['key'] = $store['key'] ?? '';
        $update['isnullable'] = $update['isnullable'] ?? '';
        $update['isignore'] = $update['isignore'] ?? '';
        $update['key'] = $update['key'] ?? '';
        $content = 
<<<EOF___
<?php

namespace App\Http\Requests{$ddl['_dir']}\\{$this->table};

use Illuminate\Foundation\Http\FormRequest;
use OpenApi\Attributes as OA;
use Illuminate\Validation\Rules\Enum;
{$enums_use}

/**
* @create {$this->date}
*/
#[OA\Schema(title: '{$ddl['comment']}表单', required: [{$store['key']}],)]
class {$this->table}Request extends FormRequest {

$property
    public function rules() {       /* 'mobile' => 'required|regex:/^1\d{10}$/' */ 
        // ignore   : {$store['isignore']}
        // nullable : {$store['isnullable']}
        return [
$rules
            // ...(config('admin.admin_catpcha') ? ['code' => 'required',] : []),
        ];
    }

    public function attributes() {  /* 'mobile' => '手机号' */ 
        return [
$rulestr
        ];
    }

    public function messages() {    /* 'mobile.regex' => '手机号格式不对' */ 
        return [
$messages
        ];
    }

}
EOF___;
        if ($this->echo) {
            echo $content; return;
        }
        file_put_contents($file, $content);
        $this->info('验证器文件生成完成 ' .$file);
    }

    /**生成vue页面
     * @param $res
     * @return void
     * @date  2023-04-13 16:16
     * @author dapeng
            // js压缩 https://tool.ip138.com/javascript/
            function deepCopy(data, hash = new WeakMap()) {
                if(typeof data !== 'object' || data === null){
                    throw new TypeError('传入参数不是对象');
                }
                // 判断传入的待拷贝对象的引用是否存在于hash中
                if(hash.has(data)) {
                    return hash.get(data);
                }
                let newData = {};
                const dataKeys = Object.keys(data);
                dataKeys.forEach(value => {
                    const currentDataValue = data[value];
                    // 基本数据类型的值和函数直接赋值拷贝 
                    if (typeof currentDataValue !== "object" || currentDataValue === null) {
                        newData[value] = currentDataValue;
                    } else if (Array.isArray(currentDataValue)) {
                        // 实现数组的深拷贝
                        newData[value] = [...currentDataValue];
                    } else if (currentDataValue instanceof Set) {
                        // 实现set数据的深拷贝
                        newData[value] = new Set([...currentDataValue]);
                    } else if (currentDataValue instanceof Map) {
                        // 实现map数据的深拷贝
                        newData[value] = new Map([...currentDataValue]);
                    } else { 
                        // 将这个待拷贝对象的引用存于hash中
                        hash.set(data,data);
                        // 普通对象则递归赋值
                        newData[value] = deepCopy(currentDataValue, hash);
                    } 
                }); 
                return newData;
            }
            // new 请求, 封装loading
            // function _useFetch(method, url, loadingStart=()=>{}, loadingEnd=()=>{}, opt={}) {
            //     let params = opt.params ?? opt.param ?? opt.query ?? {};     // uri query参数 缺省值params param query
            //     _url = `${url}${url.includes('?')?'&':'?'}${new URLSearchParams(params).toString()}`;
            //     method = (method ?? 'get').toUpperCase();    // get post put delete
            //     let body = method=='GET'?null : JSON.stringify(opt.body ?? opt.data ?? opt.payload ?? {});   // 缺省值body data payload
            //     let headers = {'Content-Type': 'application/json;charset=utf-8'};
            //     return window.VueUse.useFetch(
            //         _url, 
            //         { method, body, headers, },                   
            //         {
            //             timeout: opt.timeout ?? 1000*5,
            //             async beforeFetch({ url, options, cancel }) { loadingStart(); return { options, } },
            //             afterFetch(ctx) { loadingEnd(); return ctx },
            //             onFetchError(ctx) { console.log(ctx); loadingEnd(); return ctx },
            //         }
            //     )
            // }
            // 业务封装 返回后端的{}
            async function useFetch(method, url, opt={}, ...loadings) {
                if (loadings.filter(it=>!it).length > 0) throw Error("loading值设置错误 eg: ref, [item,'__loading']");
                const { error, data } = await _useFetch(method, url, loadingStart=()=>{ loadings.forEach(it=>it instanceof Array?it[0][it[1]]=true:it.value=true) }, loadingEnd=()=>{ loadings.forEach(it=>it instanceof Array?it[0][it[1]]=false:it.value=false) }, opt)
                                                    .json();
                if (error.value) {
                    ElementPlus.ElNotification.error({title: error.value ?? '服务器异常' });
                    throw new Error(error.value ?? '服务器异常');
                }
                if (data.value.code !== 200) {
                    ElementPlus.ElNotification.error({title: data.value.msg ?? '请求错误' });
                    throw new Error(data.value.msg ?? '请求错误');
                }
                return data.value;
            }
            // 重设表格高度
            function resetTableHigh() {
                var getHigh = el => el.offsetHeight + getComputedStyle(el, null).marginTop.replace('px', '')*1.0 + getComputedStyle(el, null).marginBottom.replace('px', '')*1.0;
                setTimeout(() => {
                    const tableHigh = window.innerHeight - document.querySelector('.el-table').offsetTop - getHigh(document.querySelector('.el-pagination').parentElement) - getHigh(document.querySelector('.main-footer')) - 40;
                    const tableHeaderHigh = getHigh(document.querySelector('.el-table__header-wrapper'));
                    document.querySelector('.el-table__body-wrapper').style.maxHeight = `${tableHigh-tableHeaderHigh}px`;
                    document.querySelector('.el-table__body-wrapper').style.overflowY = `auto`;
                }, 200);
            }
     */
    public function createVue($ddl) {
//        给vue增加一个table表的目录
        $file = $this->paths['vue_view'] . strtolower($this->true_table).'.blade.php';
        if (file_exists($file) && !$this->debug){
            $this->warn('vue文件已存在');
            return false;
        }
        $dir2 = $this->dir ? '/'.strtolower($this->dir) : '/admin';

        // queryParam
        $ddl['ddl']->filter(fn($it)=>$it['iskey']||$it['enums'])->sortByDesc('iskey')->each(function($it)use(&$queryParam,&$queryField) {
            if ($it['enums']) {
                $queryField .= <<<EOF
                    {'type': 'select', 'prop': '{$it['name']}', 'placeholder': '{$it['COLUMN_COMMENT']}', 'width': '120', 'enums': props.{$it['name']}},
EOF.PHP_EOL;
                $queryParam .= <<<EOF
            <el-form-item label="" prop="{$it['name']}">
                <el-select v-model="query.form.{$it['name']}" style="width: 120px;" placeholder="{$it['COLUMN_COMMENT']}" clearable>
                    <el-option v-for="(v,k) in props.{$it['name']}" :key="k" :label="v" :value="parseInt(k)"></el-option>
                </el-select>
            </el-form-item>
EOF.PHP_EOL;
            } else {
                $queryField .= <<<EOF
                    {'type': 'input', 'prop': '{$it['name']}', 'placeholder': '{$it['COLUMN_COMMENT']}', 'width': '120'},
EOF.PHP_EOL;
                $queryParam .= <<<EOF
            <el-form-item label="" prop="{$it['name']}">
                <el-input style="width: 120px;" clearable v-on:keyup.enter.native="handleGetList"
                    placeholder="{$it['COLUMN_COMMENT']}"
                    v-model="query.form.{$it['name']}"
                ></el-inpu>
            </el-form-item>
EOF.PHP_EOL;
            }
        });
        $queryParam = rtrim($queryParam);
        $queryField = rtrim($queryField);
        // table
        $imgstrs = $ddl['ddl']->filter(fn($it)=>$it['isimage'])->map(fn($it)=>"'{$it['name']}'")->join(',');
        $ddl['ddl']->each(function($it)use(&$table, $imgstrs) {
            $w = $it['isdatetime'] ? '160' : ($it['islongstr'] ? 'auto' : '100');
            if ($it['isimage']) {
                $table .= <<<EOF
        <el-table-column label="{$it['COLUMN_COMMENT']}" prop="{$it['name']}" width="100">
            <template v-slot:default="scope">
                <el-image fit="contain" 
                    :src="image_prefix(scope.row.{$it['name']}, true)" 
                    :preview-src-list="[$imgstrs].map(it => image_prefix(scope.row[it], true)).filter(it=>it)"
                    :initial-index="[$imgstrs].indexOf('{$it['name']}')"
                ></el-image>
            </template>
        </el-table-column>
EOF.PHP_EOL;
            } else if ($it['enums']) {
                $table .= <<<EOF
        <el-table-column label="{$it['COLUMN_COMMENT']}" prop="{$it['name']}" width="120">
            <template v-slot:default="scope">
                <!-- success 绿色 info 灰色 warning 橙色 danger 红色 primary 蓝色 -->
                <!-- :type="[1].includes(scope.row.{$it['name']})?'success':'danger'" -->
                <el-tag :type="['success','info','warning','danger','primary'][5%scope.row.{$it['name']}]">@{{ props.{$it['name']}[scope.row.{$it['name']}] }}</el-tag>
            </template>
        </el-table-column>
EOF.PHP_EOL;
            } else if ($it['ispri']) {
                $table .= <<<EOF
        <el-table-column label="{$it['COLUMN_COMMENT']}" prop="{$it['name']}" width="80" sortable="custom" fixed="left" align="center"></el-table-column>
EOF.PHP_EOL;
            } else {
                $table .= <<<EOF
        <el-table-column label="{$it['COLUMN_COMMENT']}" prop="{$it['name']}" width="{$w}" ></el-table-column>
EOF.PHP_EOL;
            }
        });
        // $table = rtrim($table);
        // dialog
        $ddl['ddl']->each(function($it)use(&$dialog,&$isimgwatch,$ddl){
            $required = $it['isnullable'] ? '' : " :rules=\"[{'required': true,},]\"";
            $disabled = $it['isignore'] ? ' disabled' : '';
            $isedit = $disabled ? " v-if=\"dialog.form.{$ddl['pri']}\"" : '';
            if ($it['isimage']) {
                $isimgwatch .= <<<EOF
            watch(() => dialog.form.{$it['name']}, (newv, oldv)=>{ setTimeout(() => {limitEnd(newv?.split(',')?.filter(it=>it), '{$it['name']}'); }, 200); }, {immediate: false, deep: false})
EOF.PHP_EOL;
                $input = <<<EOF
                        <el-upload$disabled drag list-type="picture-card" :auto-upload="true"
                            class="__upload_{$it['name']}"
                            :accept="dialog.__upload_accept_{$it['name']} ?? '.jpg,.png,.gif'"
                            :limit="dialog.__upload_limit_{$it['name']} ?? 1"
                            :file-list="dialog.form.{$it['name']}?.split(',')?.filter(it=>it)?.map(it=>{ return {'name': it, 'url': image_prefix(it)} })"
                            :action="apiUpload(dialog.form[preset.primaryKey], '{$it['name']}')"
                            :on-preview="file => handleDialogUpload('on-preview', '{$it['name']}', file)"
                            :on-remove="(file, files) => handleDialogUpload('on-remove', '{$it['name']}', file, files)"
                            :on-success="(response, file, files) => handleDialogUpload('on-success', '{$it['name']}', file, files, response)"
                            :on-error="(error, file, files) => handleDialogUpload('on-error', '{$it['name']}', file, files, null, error)"
                            :on-change="(file, files) => handleDialogUpload('on-change', '{$it['name']}', file, files)"
                            :on-execute="(file, files) => handleDialogUpload('on-execute', '{$it['name']}', file, files)"
                        >
                            <el-icon><Plus></Plus></el-icon>
                            <div class="el-upload__text">
                                Drop file here or <em>click</em>
                            </div>
                            <template v-slot:tip>
                                <div class="el-upload__tip">
                                    请上传
                                     大小不超过<b style="color:red;">@{{ dialog.__upload_filesize_{$it['name']} ?? 5 }}MB</b>
                                     <span v-if="dialog.__upload_whsize_{$it['name']}"> 尺寸为<b style="color:red;">500*500px</b></span>
                                     格式为<b style="color:red;">@{{ (dialog.__upload_accept_{$it['name']} ?? '.jpg,.png,.gif').split(',').join('/') }}</b>
                                     最多<b style="color:red;">@{{ dialog.__upload_limit_{$it['name']} ?? 1 }}个</b>
                                    文件
                                </div>
                            </template>
                        </el-upload>
EOF;
            } else if ($it['enums']) {
                $input = <<<EOF
                        <el-radio-group$disabled v-model="dialog.form.{$it['name']}" size="mini">
                            <el-radio v-for="(v, k) in props.{$it['name']}" :key="k" :label="parseInt(k)" >@{{ v }}</el-radio>
                        </el-radio-group>
EOF;
            } else if ($it['ismoney']) {
                $input = <<<EOF
                        <el-input-number$disabled v-model="dialog.form.{$it['name']}" :precision="2" :step="1" :max="9999" :min="0" placeholder="{$it['COLUMN_COMMENT']}" ></el-input-number>
EOF;
            } else if ($it['isdigit']) {
                $input = <<<EOF
                        <el-input$disabled type="number" v-model="dialog.form.{$it['name']}" placeholder="{$it['COLUMN_COMMENT']}"></el-input>
EOF;
            } else if ($it['isdatetime']) {
                $input = <<<EOF
                        <el-date-picker$disabled type="datetime" v-model="dialog.form.{$it['name']}" placeholder="{$it['COLUMN_COMMENT']}" value-format="YYYY-MM-DD HH:mm:ss" align="right" :picker-options="pickerOptions"></el-date-picker>
EOF;
            } else if ($it['istime']) {
                $input = <<<EOF
                <el-time-picker$disabled type="datetime" v-model="dialog.form.{$it['name']}" placeholder="{$it['COLUMN_COMMENT']}" value-format="HH:mm:ss"></el-time-picker>
EOF;
            } else if ($it['isdate']) {
                $input = <<<EOF
                        <el-date-picker$disabled type="date" v-model="dialog.form.{$it['name']}" placeholder="{$it['COLUMN_COMMENT']}" value-format="YYYY-MM-DD" align="right" :picker-options="pickerOptions" ></el-date-picker>
EOF;
            } else if ($it['isyear']) {
                $input = <<<EOF
                        <el-date-picker$disabled type="year" v-model="dialog.form.{$it['name']}" placeholder="{$it['COLUMN_COMMENT']}" value-format="YYYY"></el-date-picker>
EOF;
            } else {
                $_type = $it['islongstr'] ? 'textarea' : 'text';
                $input = <<<EOF
                        <el-input$disabled type="$_type" v-model="dialog.form.{$it['name']}" placeholder="{$it['COLUMN_COMMENT']}"></el-input>
EOF;
            }
            $dialog .= <<<EOF
            <el-row :gutter="20">
                <el-col :span="24"{$isedit}>
                    <el-form-item label="{$it['COLUMN_COMMENT']}" prop="{$it['name']}"{$required}>
$input
                    </el-form-item>
                </el-col>
            </el-row>
EOF.PHP_EOL;
        });
        $dialog = rtrim($dialog);
        $isimgwatch = rtrim($isimgwatch);
        // dialog form default
        $ddl['ddl']->filter(fn($it)=>$it['COLUMN_DEFAULT']!=''&&$it['COLUMN_DEFAULT']!=null)->each(function($it)use(&$formdefault){
            $_d1 = $it['isdigit'] ? '' : "'";
            $formdefault .= <<<EOF
                dialog.form.{$it['name']} = {$_d1}{$it['COLUMN_DEFAULT']}{$_d1};
EOF.PHP_EOL;
        });
        $formdefault = rtrim($formdefault);
        // consts
//         $ddl['ddl']->filter(fn($it)=>$it['enums'])->each(function($it)use(&$consts, &$constswhere){
//             $consts .= <<<EOF
//             this.consts.{$it['name']} = {};
//             getEnum('{$it['name']}').then(res => this.consts.{$it['name']} = res.data);
// EOF.PHP_EOL;
//             $constswhere .= " && this.consts.{$it['name']}.length > 0";
//         });
        // dd($ddl);

        $content = <<<EOF
<!DOCTYPE html>
<html lang="en">
        
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- <title>模版 - element plus</title> -->
    <link rel="stylesheet" href="https://zwping.gitee.io/cdn/element-plus_2.3.1_index.min.css">
    <style></style>
</head>
        
<body>
<div id="app" style="background-color: white; padding: 20px 10px; ">
    <!-- 筛选 -->
    <el-form v-on:keyup.enter.native="handleGetList" v-on:submit.native.prevent size="default" :inline="true" 
        :model="query.form" 
        :ref="el => query.ref=el" 
        v-show="query.visible"
        >
        <span v-for="item in query.field">
            <el-form-item v-if="item.type == 'input'" label="" :prop="item.prop">
                <el-input :style="'width: '+ item.width +'px;'" clearable
                    :placeholder="item.placeholder"
                    v-model="query['form'][item.prop]"
                ></el-inpu>
            </el-form-item>
            <el-form-item v-if="item.type == 'select'" label="" :prop="item.prop">
                <el-select :style="'width: '+ item.width +'px;'" clearable
                    :placeholder="item.placeholder"
                    v-model="query['form'][item.prop]" >
                    <el-option v-for="(v,k) in item.enums" :key="k" :label="v" :value="parseInt(k)"></el-option>
                </el-select>
            </el-form-item>
        </span>
        <el-form-item>
            <el-button-group>
                <el-button icon="Search" :loading="list.loading" v-on:click="handleGetList" type="primary">搜索</el-button>
                <el-button icon="Refresh" :loading="list.loading" v-on:click="query.form = {};handleGetList();">重置</el-button>
            </el-button-group>
        </el-form-item>
        <el-form-item><el-input style="display: none;" placeholder="bug单个input触发页面刷新"></el-input></el-form-item>
    </el-form>
    <!-- 批量操作 -->
    <el-row :gutter="10" class="mb8" style="margin-bottom: 10px;">
        <el-col v-if="preset.isadd" :span="1.5">
            <el-button type="primary" plain icon="Plus" size="mini"
                v-on:click="handleAdd"
                v-hasPermi="['admin:{$this->true_table}:add']"
            >新增</el-button>
        </el-col>
        <el-col v-if="preset.isedit" :span="1.5">
            <el-button type="success" plain icon="Edit" size="mini"
                :disabled="list.ids.length != 1"
                v-on:click="handleEdit(null)"
                v-hasPermi="['admin:{$this->true_table}:edit']"
            >修改</el-button>
        </el-col>
        <el-col v-if="preset.isdel" :span="1.5">
            <el-button type="danger" plain icon="Delete" size="mini"
                :disabled="!list.ids.length"
                v-on:click="dialog_muldel.visible = true"
                v-hasPermi="['admin:{$this->true_table}:remove']"
            >删除</el-button>
        </el-col>
        <el-col v-if="preset.isexport" :span="1.5">
            <el-button type="warning" plain icon="Download" size="mini"
                v-on:click="handleExport"
                v-hasPermi="['admin:{$this->true_table}:export']"
            >导出</el-button>
        </el-col>
        <right-toolbar :showSearch.sync="query.visible" v-on:queryTable="getList"></right-toolbar>
    </el-row>
    <!-- 列表 -->
    <!-- v-on:row-dblclick="handleEdit" -->
    <el-table max-height="1000" border :header-cell-style="{background:'#eef1f6',color:'#606266'}" element-loading-background="rgba(0, 0, 0, 0.1)" element-loading-text="Loading..."
            v-loading="list.loading"
            :data="list.data"
            v-on:sort-change="handleSortChangeSort" 
            v-on:selection-change="rows=>list.ids = rows.map(it=>it.{$ddl['pri']})"
        >
        <el-table-column type="selection" width="45" align="center" ></el-table-column>
{$table}
        <el-table-column label="操作" width="200" v-if="preset.isoptcol">
            <template v-slot:default="scope">
                <el-button v-on:click="handleEdit(scope.row)" v-if="preset.isedit" type="text" size="mini" icon="EditPen">编辑</el-button>
                
                <el-popconfirm v-if="preset.isdel" title="确认删除?" icon-color="red" v-on:confirm="handleDelete(scope.row)">
                    <template v-slot:reference>
                        <el-button v-if="preset.isdel" link type="danger" size="mini" icon="Delete" :loading="scope.row.__loading_del ?? false">删除</el-button>
                    </template>
                </el-popconfirm>
            </template>
        </el-table-column>
    </el-table>
    <!-- 分页 -->
    <div style="display: flex; justify-content: flex-end; margin-top: 20px;">
        <el-pagination background small
            layout="total, sizes, prev, pager, next, jumper"
            :page-sizes="[10, 20, 50, 100]"
            v-model:current-page="list.pag.page"
            v-model:page-size="list.pag.page_size"
            :total="list.pag.total"
            v-on:size-change="handleSizeChange"
            v-on:current-change="handleCurrentChange"
        ></el-pagination>
    </div>
    <!-- 弹窗 -->
    <el-dialog width="50%" :append-to-body="true" :destroy-on-close="true" :z-index="2000" align-center draggable 
        :close-on-press-escape="false"      {{-- 兼容el-image-viewer --}}
        title="{$ddl['comment']}" 
        v-model="dialog.visible" 
        >
        <el-form v-on:keyup.enter.native="handleInsertOrUpdate" v-on:submit.native.prevent
            label-width="150px"
            :model="dialog.form" 
            :ref="el => dialog.ref=el"
            >
{$dialog}
        </el-form>
        <template v-slot:footer>
            <span class="dialog-footer">
                <div v-if="preset.isdel && dialog.form[preset.primaryKey]" style="float: left; height: 32px; display: flex; align-items: center;">
                    <el-popconfirm title="确认删除?" icon-color="red" v-on:confirm="handleDelete(dialog.form)">
                        <template v-slot:reference>
                            <el-button :loading="dialog_muldel.loading" size="default" type="danger">删除 </el-button>
                        </template>
                    </el-popconfirm>
                </div>
                <el-button v-on:click="dialog.visible = false">取消</el-button>
                <el-button type="primary" :loading="dialog.loading" v-on:click="handleInsertOrUpdate">确定</el-button>
            </span>
        </template>
    </el-dialog>
    <el-dialog width="30%" :z-index="2000"
        v-model="dialog_muldel.visible"
        title="系统提示"
    >
        <el-text size="large">
            是否确认删除{$ddl['pri_comment']}为"@{{ list.ids.join() }}"的数据?
        </el-text>
        <template v-slot:footer>
            <span class="dialog-footer">
                <el-button v-on:click="dialog_muldel.visible = false">取消</el-button>
                <el-button type="danger" :loading="dialog_muldel.loading" v-on:click="handleDelete(null)">确定</el-button>
            </span>
        </template>
    </el-dialog>
    <el-image-viewer
        :ref="el => dialog_preview.ref=el" 
        v-if="dialog_preview.visible"
        :on-close="() => dialog_preview.visible = false"
        :url-list="dialog_preview.urls"
        :z-index="2001"
        :initial-index="dialog_preview.index ?? 0"
    ></el-image-viewer>
</div>


<script src="https://zwping.gitee.io/cdn/vue.3.2.45.vue.global.prod.js"></script>
<script src="https://zwping.gitee.io/cdn/element-plus.2.3.1.index.full.min.js"></script>
<script src="https://zwping.gitee.io/cdn/element-plus.icons-vue.2.1.0.index.iife.min.js"></script>
<script src="https://zwping.gitee.io/cdn/element-plus-2.3.6_dist_locale_zh-cn.js"></script>

<script src="https://zwping.gitee.io/cdn/vueuse.shared@9.12.0.index.iife.min.js"></script>
<script src="https://zwping.gitee.io/cdn/vueuse.core@9.12.0.index.iife.min.js"></script>

<script>
    const {
        createApp,
        ref,
        reactive,
        onMounted,
        computed,
        isRef,
        watch,
    } = Vue;
    const { Refresh, Search, EditPen, Delete, Plus, Edit, Download, } = ElementPlusIconsVue
    const { useFetch, } = window.VueUse
    
    createApp({
        setup() {
            // api统一管理
            async function apiGetList(params, ...loadings) { return useFetch('get', preset.url, {params}, ...loadings) }
            async function apiInsert(form, ...loadings) { return useFetch('post', preset.url, {'body': form}, ...loadings) }
            async function apiUpdate(id, form, ...loadings) { return useFetch('put', `\${preset.url}/\${id}`, {'body': form}, ...loadings) }
            async function apiDelete(id, ...loadings) { return useFetch('delete', `\${preset.url}/\${id}`, {}, ...loadings) }
            // async function apiExport()
            function apiUpload(id, type) { return `\${preset.url}/upload?id=\${id}&type=\${type}` }

            // ==============================

            // 外部传值
            const props = JSON.parse('{!! \$props !!}')
            // 预设数据
            const preset = JSON.parse('{!! \$preset !!}')
            // 筛选表单
            const query = reactive({
                ref: {},
                visible: true,
                field: [            // 表单字段(或后台下发) type: input/select
                    // input: {'type': 'input', 'prop': 'id', 'placeholder': 'ID', 'width': '120'},
                    // select: {'type': 'select', 'prop': 'status', 'placeholder': '停用状态', 'width': '120', 'enums': props.status},
{$queryField}
                ],
                form: { /*{$ddl['pri']}...*/ },
            })
            // 列表相关
            const list = reactive({
                loading: false,
                data: [],   // 数据源
                pag: {
                    page: 1,
                    page_size: 20,
                    total: 0,
                },
                ids: [],    // 多选框
            })
            // 弹窗-新增/修改相关
            const dialog = reactive({
                ref: {},
                visible: false,                                 // 可见
                loading: false,
                form: {},                                       // 表单内容
                // __upload_limit_banner: 2,                    // banner对应文件最多选择2个 - 默认1个文件
                // __upload_filesize_banner: 1,                 // banner对应文件大小最大1M - 默认不限制大小
                // __upload_whsize_banner: '500*500',           // banner对应图片尺寸必须为500*500 - 默认不限制
                // __upload_accept_banner: '.jpg,.png,.gif',    // banner默认文件类型为jpg/png/gif的图片 - 默认为此类型图片
            })
            // 弹窗-多选删除确认
            const dialog_muldel = reactive({
                ref: {},
                visible: false,
                loading: false,
                // form: {}, // list.ids
            })
            // elupload图片预览
            const dialog_preview = reactive({
                ref: {},
                visible: false,
                urls: [],
            })
            const pickerOptions = {
                shortcuts: [{
                    text: '今天',
                    onClick(picker) {
                        picker.\$emit('pick', new Date());
                    }
                }, {
                    text: '昨天',
                    onClick(picker) {
                        const date = new Date();
                        date.setTime(date.getTime() - 3600 * 1000 * 24);
                        picker.\$emit('pick', date);
                    }
                }, {
                    text: '一周前',
                    onClick(picker) {
                        const date = new Date();
                        date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
                        picker.\$emit('pick', date);
                    }
                }, ]
            }
            const handleCurrentChange = it => { list.pag.page = it; handleGetList() }
            const handleSizeChange = it => { list.pag.page_size = it; handleGetList() }

            // new 请求, 封装loading 源码在AdminCode.php中
            function _useFetch(method,url,loadingStart=()=>{},loadingEnd=()=>{},opt={}){let params=opt.params??opt.param??opt.query??{};_url=`\${url}\${url.includes('?')?'&':'?'}\${new URLSearchParams(params).toString()}`;method=(method??'get').toUpperCase();let body=method=='GET'?null:JSON.stringify(opt.body??opt.data??opt.payload??{});let headers={'Content-Type':'application/json;charset=utf-8'};return window.VueUse.useFetch(_url,{method,body,headers,},{timeout:opt.timeout??1000*5,async beforeFetch({url,options,cancel}){loadingStart();return{options,}},afterFetch(ctx){loadingEnd();return ctx},onFetchError(ctx){console.log(ctx);loadingEnd();return ctx},})}
            // 业务封装 返回后端的{}  // js压缩 https://tool.ip138.com/javascript/
            async function useFetch(method,url,opt={},...loadings){if(loadings.filter(it=>!it).length>0)throw Error("loading值设置错误 eg: ref, [item,'__loading']");const{error,data}=await _useFetch(method,url,loadingStart=()=>{loadings.forEach(it=>it instanceof Array?it[0][it[1]]=true:it.value=true)},loadingEnd=()=>{loadings.forEach(it=>it instanceof Array?it[0][it[1]]=false:it.value=false)},opt).json();if(error.value){ElementPlus.ElNotification.error({title:error.value??'服务器异常'});throw new Error(error.value??'服务器异常');}if(data.value.code!==200){ElementPlus.ElNotification.error({title:data.value.msg??'请求错误'});throw new Error(data.value.msg??'请求错误');}return data.value}
            function deepCopy(data,hash=new WeakMap()){if(typeof data!=='object'||data===null){throw new TypeError('传入参数不是对象');}if(hash.has(data)){return hash.get(data)}let newData={};const dataKeys=Object.keys(data);dataKeys.forEach(value=>{const currentDataValue=data[value];if(typeof currentDataValue!=="object"||currentDataValue===null){newData[value]=currentDataValue}else if(Array.isArray(currentDataValue)){newData[value]=[...currentDataValue]}else if(currentDataValue instanceof Set){newData[value]=new Set([...currentDataValue])}else if(currentDataValue instanceof Map){newData[value]=new Map([...currentDataValue])}else{hash.set(data,data);newData[value]=deepCopy(currentDataValue,hash)}});return newData}
            // 重设表格高度
            function resetTableHigh(){var getHigh=el=>el.offsetHeight+getComputedStyle(el,null).marginTop.replace('px','')*1.0+getComputedStyle(el,null).marginBottom.replace('px','')*1.0;setTimeout(()=>{const tableHigh=window.innerHeight-document.querySelector('.el-table').offsetTop-getHigh(document.querySelector('.el-pagination').parentElement)-getHigh(document.querySelector('.main-footer'))-40;const tableHeaderHigh=getHigh(document.querySelector('.el-table__header-wrapper'));document.querySelector('.el-table__body-wrapper').style.maxHeight=`\${tableHigh-tableHeaderHigh}px`;document.querySelector('.el-table__body-wrapper').style.overflowY=`auto`},200)}

            // 检查image图片链接是否有http前缀
            function image_prefix(url, oss=false) {
                if (!url) { return '' }
                url = url.split(',')[0]
                return /^https?:\/\//.test(url) ? url : `\${oss ? preset.ossurl : preset.appurl}\${url}`
            }
            // 获取当前行 let _item = getCurrItem(item)
            const getCurrItem = (item) => item ? list.data.filter(it => it[preset.primaryKey] == item[preset.primaryKey])[0] : null
            // 获取列表
            const handleGetList = async (it)=>{
                if (it) list.pag.page = 1     // 点击搜索 默认第一页

                const data = await apiGetList({...query.form, ...list.pag}, [list, 'loading'])
                
                list.data = data.data.data
                list.pag = data.data.page
            }
            // 新增
            function handleAdd() {
                for(let k in dialog.form){ delete dialog.form[k] }
{$formdefault}
                dialog.visible=true
            }
            // 修改
            const handleEdit = (item) => { 
                if (!preset.isedit){ return; } 
                if (!item) item = list.data.filter(it => it[preset.primaryKey] == list.ids[0])[0]
                dialog.form=deepCopy(item)
                dialog.visible=true 
            }
            // 新增/修改表单提交
            const handleInsertOrUpdate = async () => {
                valid = await dialog.ref.validate((valid, fields)=>{
                    if (!valid) ElementPlus.ElMessage.error({message:Object.values(fields)[0][0].message})
                })
                if (!valid) return

                let isedit = dialog.form[preset.primaryKey]    // 主键不为空, 为编辑item
                const data = await (isedit ? apiUpdate(dialog.form[preset.primaryKey], dialog.form, [dialog, 'loading']) : apiInsert(dialog.form, [dialog, 'loading']))

                dialog.visible = false
                if (isedit) {
                    Object.assign(getCurrItem(dialog.form), data.data)  // 局部刷新 触发es6 setter
                } else {
                    list.pag.current = 1; handleGetList()
                }
                return false
            }
            // 删除
            const handleDelete = async (item) => {
                let ids = item ? item[preset.primaryKey] : list.ids.join()
                const data = await apiDelete(ids, ...[[dialog_muldel, 'loading'], item ? [item,'__loading_del'] : null].filter(it => it))
                list.data.forEach((k,i) => { if (`\${ids}`.split(',').includes(`\${k[preset.primaryKey]}`)) { delete list.data[i] } })
                if (!list.data) {
                    handleGetList()
                } else {
                    list.pag.total = list.pag.total-1
                }
                ElementPlus.ElNotification.success({title: '删除成功'})
                dialog_muldel.visible = false
                dialog.visible = false
            }

            // el-upload初始化 隐藏add-dom
{$isimgwatch}
            // el-upload超出limit隐藏add-dom
            function limitEnd(files, type) {
                let limit_end = (files?.length ?? 0) >= (dialog[`__upload_limit_\${type}`] ?? 1);
                document.querySelector(`.__upload_\${type}`).querySelector('.el-upload').style.display = limit_end ? 'none' : '';
            }
            // el-upload对应事件
            const handleDialogUpload = async (event, type, file, files=null, response=null, error=null) => {
                // console.log(event, type, file, files, dialog);
                switch(event) {
                    case 'on-preview':
                        dialog_preview.index = dialog.form[type].split(',').indexOf(file.name)
                        dialog_preview.urls = dialog.form[type].split(',').filter(it=>it).map(it=>image_prefix(it))
                        dialog_preview.visible = true
                        // onClose失效 & esc失效也处理了
                        setTimeout(() => { document.querySelector('.el-image-viewer__close').onclick = ()=>{ dialog_preview.visible = false } }, 200);
                        break
                    case 'on-remove':
                        limitEnd(files, type)
                        dialog.form[type] = files.map(it=>it.name).join()
                        break
                    case 'on-success':
                        if (response.code != 200) {
                            files.pop(); ElementPlus.ElMessage.error({message:response.msg || '上传失败'});
                            return
                        }
                        dialog.form[type] = `\${dialog.form[type] ?? ''}\${dialog.form[type] && ','.indexOf(dialog.form[type]) == -1 ? ',' : ''}\${response.data.url}`
                        break
                    case 'on-error':
                        ElementPlus.ElMessage.error({message:error.message || '上传失败'})
                        break
                    case 'on-progress':
                        break
                    case 'on-change':
                        if (file.status == 'ready') {
                            // 有限制文件大小
                            if (dialog[`__upload_filesize_\${type}`]) {
                                if (file.size/1024/1024 >= dialog[`__upload_filesize_\${type}`]) {
                                    files.pop(); ElementPlus.ElMessage.error({message:`文件大小超过了\${dialog[`__upload_filesize_\${type}`]}MB`});
                                    return
                                }
                            }
                            // 有限制图片尺寸 eg: '500*500'
                            if (dialog[`__upload_whsize_\${type}`]) {
                                const img = new Image()
                                const promise = new Promise((resolve, reject) => {
                                    img.onload = () => resolve({width: img.width, height: img.height});
                                    img.onerror = () => resolve(new Error('Failed to load image'));
                                });
                                img.src = file.url
                                const result = await promise
                                if (result instanceof Error) {
                                    files.pop(); ElementPlus.ElMessage.error({message:result.message});
                                    return
                                }
                                const _wh = dialog[`__upload_whsize_\${type}`].split('*')
                                if (result.width != _wh[0] || result.height != _wh[1]) {
                                    files.pop(); ElementPlus.ElMessage.error({message:`图片尺寸不符合要求 \${JSON.stringify(result)}`});
                                    return 
                                }
                            }
                        }
                        limitEnd(files, type)
                        break
                    case 'on-exceed':
                        ElementPlus.ElMessage.warning({message:`最多\${dialog[`__upload_limit_\${type}`] ?? 1}个文件`})
                        break
                    case 'before-upload':
                        break
                    case 'before-remove':
                        break
                }
            }

            // 导出csv表格
            const handleExport = (_type='') => {
                let url = preset.url+`/export?_type=\${_type}&`+Object.entries(query.form).map(([k, v]) => `\${encodeURIComponent(k)}=\${encodeURIComponent(v)}`).join('&')
                window.open(url)
            }

            // el-upload中el-image-viewer onClose&esc失效
            window.addEventListener('keydown', (e) => {
                if (e.keyCode === 27) { // esc
                    if (dialog_preview.visible) { dialog_preview.visible = false } 
                    else if (dialog.visible) { dialog.visible = false }
                }
            });
            handleGetList()
            resetTableHigh()

            return {
                props,                  // 外部传值
                preset,                 // 预设数据
                query,                  // 筛选表单
                list,                   // 列表相关
                dialog,                 // 弹窗-新增修改相关
                dialog_muldel,          // 弹窗-多选删除确认
                dialog_preview,         // elupload图片预览

                image_prefix,           // 兼容APPURL/OSSURL 图片前缀

                apiUpload,              // 图片上传api
                handleDialogUpload,     // elupload对应事件

                handleExport,           // 导出csv表格

                // CURD
                handleGetList, handleCurrentChange, handleSizeChange,
                handleAdd, handleEdit, handleInsertOrUpdate,
                handleDelete,
            }
        },
    })
    .use(ElementPlus, {
        locale: ElementPlusLocaleZhCn,
    })
    .component('Refresh', Refresh)
    .component('Search', Search)
    .component('EditPen', EditPen)
    .component('Delete', Delete)
    .component('Plus', Plus)
    .component('Edit', Edit)
    .component('Download', Download)
    .mount('#app');
</script>
</body>

<style scoped>
    /deep/ .el-dialog._body { max-height: 80vh; overflow-y: auto; }
    /deep/ .el-image { height: 80px; display: flex; align-items: center; justify-content: center; }
</style>
</html>
EOF;
        if ($this->echo) {
            echo $content; return;
        }
        file_put_contents($file, $content);
        $this->info('vue文件生成完成');
    }

    /**生成vue请求
     * @param $res
     * @return void
     * @date  2023-04-13 16:16
     * @author dapeng
     */
    public function createVueRequest($ddl) {
        $file = $this->paths['vue_request'] . $this->true_table.'.js';
        if (file_exists($file) && !$this->debug){
            $this->warn('vue_js文件已存在');
            return false;
        }
        if ($this->dir) $this->dir .= '/';
        $this->dir = strtolower($this->dir);

        $content = <<<EOF
import request from '@/utils/request'

// 查询列表
export function list{$this->table}(query) {
  return request({
    url: '/admin/{$this->dir}{$this->true_table}',
    method: 'get',
    params: query
  })
}

// 查询详细
export function get{$this->table}({$ddl['pri']}) {
  return request({
    url: '/admin/{$this->dir}{$this->true_table}/' + {$ddl['pri']},
    method: 'get'
  })
}

// 新增
export function add{$this->table}(data) {
  return request({
    url: '/admin/{$this->dir}{$this->true_table}',
    method: 'post',
    data: data
  })
}

// 修改
export function update{$this->table}(data) {
  return request({
    url: '/admin/{$this->dir}{$this->true_table}/'+data.{$ddl['pri']},
    method: 'put',
    data: data
  })
}

// 删除
export function del{$this->table}({$ddl['pri']}s) {
    return request({
        url: '/admin/{$this->dir}{$this->true_table}/' + {$ddl['pri']}s,
        method: 'delete'
    })
}

// 获取一些下拉选择 枚举类
export function getEnum(type) {
    return request({
        url: `/admin/{$this->dir}{$this->true_table}/enum/\${type}`,
        method: 'get'
    })
}

// excel文件导出
export function export{$this->table}(query) {
    var url = process.env.VUE_APP_API_DOMAIN + '/admin/{$this->dir}{$this->true_table}/export?'
    // for (var i in query){
    //   url += i + '=' + query[i] + '&';
    // }
    console.log(url)
    window.open(url, '_blank')
}

EOF;

        if ($this->echo) {
            echo $content; return;
        }
        file_put_contents($file, $content);
        $this->info('js文件生成成功');
    }

}
